import couchdb
import optparse
import sys
import hashlib
import httplib2
import settings
import memcache
import uuid
import pickle
from Crypto.Util import randpool
from Crypto.PublicKey import DSA
import datetime
import random
import os
import threading
import simplejson as sj
import Queue
import types
rand = random.SystemRandom()

seed = datetime.datetime.now().second

def error(message,fail=True):
    print >> sys.stderr, message
    if fail:
        sys.exit(-1)

def getmd5(value):
    m = hashlib.md5()
    m.update(value)
    return m.hexdigest()

def connect():
    host = settings.COUCHDB_HOST
    port = settings.COUCHDB_PORT
    
    try:
        server = couchdb.Server('http://%s:%d'%(host,port))
    except httplib2.ServerNotFoundError,e:
        error('Couch db server http://%s:%d not found.'%(host,port))
    try:
        dbname = settings.COUCHDB_DATABASE
        if dbname not in server:
            db = server.create(dbname)
        else:
            db = server[dbname]
    except:
        error('Not able to connect with couchdb server')
    return db

def memconnect():
    hosts = settings.MEMCACHE_HOSTS
    c = memcache.Client(hosts)
    c.set('test','testing')
    if c.get('test') != 'testing':
        error('Not able to connect with memcache servers.')
    c.delete('test')
    return c
    
def main():
    db = connect()
    mem = memconnect()

    #should begin main loop here, also need a port and host to listen to

def getrandlong(dsa):
    return long(rand.randrange(2,dsa.q))

def getrandmd5():
    m = getmd5(os.urandom(256))
    return m



class MemcacheHandler(object):
    def __init__(self,mem):
        self.mem = mem
    def get_account(self,loginhash):
        return self.mem.get('%s-account'%loginhash)
    def update_account(self,loginhash,value):
        self.mem.set('%s-account'%loginhash,value)
    def get_session(self,loginhash):
        return self.mem.get('%s-session'%loginhash)
    def update_session(self,loginhash,randhash):
        self.mem.set('%s-session'%loginhash,randhash)
    def delete_session(self,loginhash):
        self.mem.delete('%s-session'%loginhash)
    def delete_account(self,loginhash):
        self.mem.delete('%s-account'%loginhash)

reserved_words = ['password','loginhash','last_login','created_at','type','session']
publishedfunctions = []
def publicfunc(f):
    publishedfunctions.append(f.__name__)
    def cleanoutput(output):
        if 'password' in output:
            del output['password']
        if 'loginhash' in output:
            del output['loginhash']
        return output
    def func(self,*args,**kwargs):
        output = f(self,*args,**kwargs)
        if type(output) == couchdb.client.ViewResults:
            output = list(output)
            for j,item in enumerate(output):
                i = item['value']
                output = cleanoutput(i)
        if type(output) == dict:
            output = cleanoutput(output)
        if type(output) == couchdb.client.Document:
            output = cleanoutput(dict(output))
        return output
        
    return func


class AccountManager(object):
    def __init__(self,db,mem):
        self.db = db
        self.mem = MemcacheHandler(mem)
        self.dsa = pickle.loads(settings.DSA_KEY)
    def _get_random_loginhash(self,loginhash):
        m = hashlib.md5()
        m.update(loginhash)
        for i in range(0,5):
            m.update(getrandmd5())
        return m.hexdigest()
    def _get_loginhash(self,login):
        return getmd5(login)
    def _get_account(self,loginhash):
        a = self.mem.get_account(loginhash)
        if a:
            return a
        v = self.db.view('accounts/list',key=loginhash)
        if v:
            a = dict(v.rows[0])
            self.mem.update_account(loginhash,a['value'])
            return a['value']
        else:
            return None
    def _update_account(self,loginhash,values):
        account = self._get_account(loginhash)
        if account:
            account = self.db[account['_id']]
            self.mem.update_account(loginhash,self.db[account['_id']])
            for word in reserved_words:
                if word in values:
                    del values[word]
            for key in values:
                if key in account:
                    if account[key] != values[key]:
                        account[key] = values[key]
                else:
                    account[key] = values[key]
            self.db.update([account])
            return True
        return False
    @publicfunc
    def update_account(self,login,values):
        if type(values) != type({}):
            try:
                values = sj.loads(values)
                if type(values) != type({}):
                    return (-1,'Values must be of type dict')
            except:
                return (-1,'Values must be of type dict')

        loginhash = self._get_loginhash(login)
        ret = self._update_account(loginhash,values)
        if ret == False:
            return (-1,'Account not found')
        return (0,True)

    @publicfunc
    def change_password(self,login,password):
        account = self.get_account(login)
        if not account:
            return False
        account = self.db[account['_id']]
        account['password'] = getmd5(password)
        self.db.update([account])
        self.mem.update_account(self._get_loginhash(login),account)
        return True
    def _create_account(self,loginhash,login,password,values):
        for word in reserved_words:
            if word in values:
                del values[word]

        account = self.db.create({'login':login,
                   'type':'account',
                   'loginhash':loginhash,
                   'password':getmd5(password)})
        account = self.db[account]
        for key in values:
            account[key] = values[key]
        return account

    @publicfunc
    def create_account(self,login,password,values):
        if type(values) != type({}):
            return None
        loginhash = self._get_loginhash(login)
        account = self._get_account(loginhash)
        if account:
            return account
        else:
            return self._create_account(loginhash,login,password,values)
    @publicfunc
    def get_account(self,login):
        loginhash = self._get_loginhash(login)
        return self._get_account(loginhash)
    @publicfunc
    def authenticate(self,login,password):
        loginhash = self._get_loginhash(login)
        account = self._get_account(loginhash)
        if not account:
            raise Exception('Account not found')
        if getmd5(password) == account['password']:
            #need to recreate session since we are authentication with login and password, not a session key
            return self._create_session(loginhash)
        else:
            raise Exception('Invalid password')
    def _update_session(self,loginhash,randhash):
        self.mem.update_session(loginhash,randhash)
        account = self._get_account(loginhash)
        if not account:
            return False
        acc = self.db[account['_id']]
        if 'session' not in acc:
            acc['session'] = self._create_session_object(account['_id'],randhash)
            self.db.update([acc])
        else:
            sess = self.db[acc['session']]
            sess['randhash'] = randhash
            self.db.update([sess])
        self.mem.update_account(loginhash,acc)
    def _create_session_object(self,accountid,randhash):
        sess = self.db.create(
            {'account':accountid,
             'randhash':randhash,
             'type':'session'
            }
        )
        return sess

    def _generate_session_key(self,randhash):
        return sj.dumps(self.dsa.sign(randhash,getrandlong(self.dsa))).encode('base64')

    def _get_session(self,loginhash):
        sess = self.mem.get_session(loginhash)
        if sess:
            return sess
        account = self._get_account(loginhash)
        if not account:
            return None
        if 'session' in account:
            sess = self.db[account['session']]
            self.mem.update_session(loginhash,sess['randhash'])
            return sess['randhash']
        return None
    def _new_session_key(self,loginhash):
        sess = self._get_session(loginhash)
        if not sess:
            raise Exception('Session object not found. You must authenticate first.')
        return self._generate_session_key(sess)

    @publicfunc
    def new_session_key(self,login):
        loginhash = self._get_loginhash(login)
        return self._new_session_key(loginhash)
    def _verify_key(self,sessionkey,randhash):
        try:
            sessionkey = sessionkey.decode('base64')
        except:
            return False
        try:
            key = sj.loads(sessionkey)
        except:
            return False
        try:
            return self.dsa.verify(randhash,key)
        except:
            return False
    def _delete_account(self,loginhash):
        self.mem.delete_account(loginhash)
        account = self._get_account(loginhash)
        if not account:
            return False
        account = self.db[account['_id']]
        self.db.delete(account)
        return True
        

    @publicfunc
    def delete_account(self,login):
        return self._delete_account(self._get_loginhash(login))


    def _create_session(self,loginhash):
        randhash = self._get_random_loginhash(loginhash)
        self._update_session(loginhash,randhash)
        return self._generate_session_key(randhash)
    def _verify_session(self,loginhash,sessionkey):
        sess = self._get_session(loginhash)
        if not sess:
            return False
        else:
           return self._verify_key(sessionkey,sess)
    def _delete_session(self,loginhash):
        self.mem.delete_session(loginhash)
        account = self._get_account(loginhash)
        if not account:
            return False
        if 'session' in account:
            sessid = account['session']
            sess = self.db[sessid]
            self.db.delete(sess)
            account = dict(self.db[account['_id']])
            account.pop('session')
            self.db.update([account])
            self.mem.update_account(loginhash,account)
            return True
        return False

    @publicfunc
    def delete_session(self,login):
        return self._delete_session(self._get_loginhash(login))

    @publicfunc
    def verify_session(self,login,sessionkey):
        return self._verify_session(self._get_loginhash(login),sessionkey)

class Handler(object):
    def __init__(self,queue,returnqueue):
        c = connect()
        m = memconnect()
        self.am = AccountManager(c,m)
        self.queue = queue
        self.returnqueue = returnqueue
    def handle(self):
        while 1:
            callid,func,args = self.queue.get()
            if type(args) != types.TupleType:
                try:
                    args = tuple(args)
                except:
                    self.returnqueue.put((callid,-1,'Args must be a tuple.'))
                    continue
            for item in args:
                if type(item) != type(u'') and type(item) != type('') and type(item) != None:
                    self.returnqueue.put((callid,-1,'Args must contain only string and unicode objects.'))
                    continue
            if type(func) != type(u'') and type(func) != type(''):
                self.returnqueue.put((callid,-1,'Function must be type string or unicode.'))
                continue
            if func in publishedfunctions:
                f = getattr(self.am,func)
                try:
                    r = f(*args)
                except Exception, e:
                    self.returnqueue.put((callid,-1,str(e)))
                    continue
                self.returnqueue.put((callid,0,r))
            else:
                self.returnqueue.put((callid,-1,'Requested function is not a valid function.'))

    

class QueueHandler(object):
    def __init__(self):
        self.queue,self.returnqueue = self.launchhandlers()
        self.funccalls = {}
        self.funcreturns = {}
        self.startwatcher()
    def getuuid(self):
        return str(uuid.uuid4())
    def launchhandlers(self):
        q = Queue.Queue()
        rq = Queue.Queue()
        for i in range(0,settings.HANDLER_INSTANCES):
            h = Handler(q,rq)
            t = threading.Thread(None,h.handle)
            t.setDaemon(True)
            t.start()
        return q,rq
    
    def process(self,func,args):
        uid = self.getuuid()
        l = threading.Event()
        self.funccalls[uid] = l
        self.queue.put((uid,func,args))
        l.wait()
        ret = self.funcreturns.get(uid,None)
        try:
            del self.funcreturns[uid]
        except:
            pass
        if ret:
            return sj.dumps(ret[1:])
    def _watcher(self):
        while 1:
            ret = self.returnqueue.get()
            try:
                uid = ret[0]
            except:
                continue
            self.funcreturns[uid] = ret
            l = self.funccalls.get(uid,None)
            if l:
                l.set()
            try:
                del self.funccalls[uid]
            except:
                continue

    def startwatcher(self):
        t = threading.Thread(None,self._watcher)
        t.setDaemon(True)
        t.start()

queuehandler = QueueHandler()

class ClientHandler(object):
    def __init__(self):
        self.clients = {}
        self.sessions = {}
        self.dsakeys = {}
        self.dsa = pickle.loads(settings.DSA_KEY)
        self.rsa = pickle.loads(settings.RSA_KEY)
    def get_initial(self,name):
        loginhash = getmd5(name)
        for d in [self.sessions,self.clients,self.dsakeys]:
            if loginhash in d:
                del d[loginhash]
        ran = getrandmd5()
        self.sessions[loginhash] = ran
        return ran
    def verify_key(self,name,key,dsapub):
        loginhash = getmd5(name)
        if loginhash not in self.sessions:
            return (-1,'Session does not exist or has been validated. Please initialize your session first.')
        try:
            rsa = self.rsa.decrypt(key)
            if rsa != self.sessions[loginhash]:
                return (-2,'Unable to load dsa key.')
            key = sj.loads(dsapub)
            dsa = DSA.DSAobj()
            dsa.__setstate__(key)
            del self.sessions[loginhash]
        except:
            return False
        self.dsakeys[loginhash] = dsa
        ran = getrandmd5()
        self.clients[loginhash] = ran
        return ran
    def verify_session(self,name,rand):
        loginhash = getmd5(name)
        if loginhash not in self.clients or loginhash not in self.dsakeys or loginhash in self.sessions:
            return False
        try:
            signed = sj.loads(rand)
            dsa = self.dsakeys[loginhash]
            ran = self.clients[loginhash]
            return dsa.verify(ran,signed)
        except:
            return False

#todo - make the return from the queue a list, not a tuple - sj.dumps
def testclienthandler():
    c = ClientHandler()
    rsa = pickle.loads(settings.RSA_KEY)
    dsa = pickle.loads(settings.DSA_KEY)
    s = 'jeremy'

    initial = c.get_initial(s)
    sessionkey =  c.verify_key(
        s,
        rsa.encrypt(initial,''),
        sj.dumps(dsa.publickey().__getstate__())
    )

    print c.verify_session(s,sj.dumps(dsa.sign(sessionkey,getrandlong(dsa))))







def commands():
    return publishedfunctions



